﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.IO;
using System.Text.RegularExpressions;

namespace ErikLieben.CssPreprocessor
{
    /// <summary>
    /// Prototype class only need refactoring
    /// </summary>
    public class JavascriptCssVariablesHttpHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        /// <summary>
        /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler"/> interface.
        /// </summary>
        /// <param name="context">An <see cref="T:System.Web.HttpContext"/> object that provides references to the intrinsic server 
        /// objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param>
        public void ProcessRequest(HttpContext context)
        {
            if (!context.Request.Url.AbsoluteUri.ToLowerInvariant().Contains("eriklieben.cssoptimizer.js") || !context.Request.Url.AbsoluteUri.ToLowerInvariant().EndsWith(".css")) return;

            // TODO: try get? will this thrown an exception?
            // retrieve and cleanup the code for processing
            string cssCode = RetrieveLocalCss(context.Server.UrlDecode(context.Request.QueryString.Get("css")));
            cssCode = Cleanup.CleanUpCss(cssCode);

            // Detect browser and add default settings
            Dictionary<string, string> variables = CreateUserAgentVariables(context.Request.UserAgent, new Dictionary<string, string>());
            // Process variable declarations
            variables = ProcessVariables(cssCode, variables);
            //cssCode = ProcessConditions(cssCode, variables);

            var cssDefinitions = BuildCssDefinition(cssCode);

            string jsCode = string.Empty;
            jsCode += "var debug = '';";
            jsCode += "document.css = {};" + Environment.NewLine;
            List<string> smartVariables = new List<string>();
            foreach (string key in variables.Keys)
            {
                if (key.Contains("."))
                {
                    string w = key.Split(new[] { '.' })[0];
                    if (!smartVariables.Contains(w)) smartVariables.Add(w);
                }
            }
            foreach (string s in smartVariables)
            {
                jsCode += "document.css." + s + " = {};" + Environment.NewLine;
            }
            foreach (string key in variables.Keys)
            {
                if (key.Contains("."))
                {
                    string[] keyParts = key.Split(new[] { '.' });
                    jsCode += Environment.NewLine;
                    jsCode += "(function(){" + Environment.NewLine;
                    jsCode += "  var val = \"" + variables[key] + "\";" + Environment.NewLine;
                    jsCode += "  Object.defineProperty( document.css." + keyParts[0] + ", \"" + keyParts[1] + "\", {" + Environment.NewLine;
                    jsCode += "    get: function(){ return val; }," + Environment.NewLine;
                    jsCode += "    set: function(value){ " + Environment.NewLine;

                    foreach (var cssDef in cssDefinitions)
                    {
                        var properties = cssDef.Properties.Where(f => f.Value == key);
                        foreach (var property in properties)
                        {
                            foreach (var selector in cssDef.Selectors)
                            {
                                jsCode += "		$(\"" + selector.Name + selector.Pseudo + "\").css(\"" + property.Name + "\", value);" + Environment.NewLine;
                            }
                        }
                    }

                    jsCode += "		val = value; " + Environment.NewLine;
                    jsCode += "	    }" + Environment.NewLine;
                    jsCode += "  });" + Environment.NewLine;
                    jsCode += "})();" + Environment.NewLine;
                    jsCode += Environment.NewLine;

                    jsCode += "debug += '" + key + " => " + variables[key] + "<br/>';" + Environment.NewLine;
                }
                else
                {
                    jsCode += Environment.NewLine;
                    jsCode += "(function(){" + Environment.NewLine;
                    jsCode += "  var val = \"" + variables[key] + "\";" + Environment.NewLine;
                    jsCode += "  Object.defineProperty( document.css, \"" + key + "\", {" + Environment.NewLine;
                    jsCode += "    get: function(){ return val; }," + Environment.NewLine;
                    jsCode += "    set: function(value){ " + Environment.NewLine;

                    foreach (var cssDef in cssDefinitions)
                    {
                        var properties = cssDef.Properties.Where(f => f.Value == key);
                        foreach (var property in properties)
                        {
                            foreach (var selector in cssDef.Selectors)
                            {
                                jsCode += "		$(\"" + selector.Name + selector.Pseudo + "\").css(\"" + property.Name + "\", value);" + Environment.NewLine;
                            }
                        }
                    }

                    jsCode += "		val = value; " + Environment.NewLine;
                    jsCode += "	    }" + Environment.NewLine;
                    jsCode += "  });" + Environment.NewLine;
                    jsCode += "})();" + Environment.NewLine;
                    jsCode += Environment.NewLine;
                    jsCode += "debug += '" + key + " => " + variables[key] + "<br/>';" + Environment.NewLine;
                }
            }
            //
            SetHeaders(jsCode.GetHashCode(), context);
            context.Response.Write(jsCode);
            //Compress(context);

        }

        private static void SetHeaders(int hash, HttpContext context)
        {
            context.Response.ContentType = "text/javascript";
            context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;

            context.Response.Cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
            context.Response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
            context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);

            string etag = "\"" + hash.ToString() + "\"";
            string incomingEtag = context.Request.Headers["If-None-Match"];

            context.Response.Cache.SetETag(etag);

            if (String.Compare(incomingEtag, etag) == 0)
            {
                context.Response.Clear();
                context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
                context.Response.SuppressContent = true;
            }
        }




        /// <summary>
        /// Retrieves the local CSS from the disk
        /// </summary>
        /// <param name="file">The file to load the css code from.</param>
        /// <returns>the retrieved css as string</returns>
        private static string RetrieveLocalCss(string file)
        {
            string path = HttpContext.Current.Server.MapPath(file);
            try
            {
                return File.ReadAllText(path);
            }
            catch
            {
                // TODO: Figure out what a nice way of error handling would be
                return string.Empty;
            }
        }

        /// <summary>
        /// Processes the variables in a given string.
        /// </summary>
        /// <param name="css">The CSS to process.</param>
        /// <returns>a dictionary of variables found</returns>
        public static Dictionary<string, string> ProcessVariables(string css, Dictionary<string, string> variables)
        {
            foreach (Match match in Regex.Matches(css, "@variable (?<name>.*?)\\{(?<inner>.*?)\\}"))
            {
                string variableName = match.Groups["name"].Value.Trim();
                string[] subVars = match.Groups["inner"].Value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);

                if (subVars.Length < 1) // say what?
                {
                    //Trace.WriteLine("Error parsing variable " + variableName);
                    continue;
                }

                if (subVars.Length == 1) // single variable definition
                {
                    if (variables.ContainsKey(variableName))
                    {
                        variables[variableName] = subVars[0];
                    }
                    else
                    {
                        variables.Add(variableName, subVars[0]);
                    }
                }
                else // multi variable definition
                {
                    foreach (string subVar in subVars)
                    {
                        string[] subVarDef = subVar.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
                        if (subVarDef.Length < 1) // what?
                        {
                            //Trace.WriteLine("Error parsing multi variable string " + subVars);
                            continue;
                        }

                        string key = variableName + "." + subVarDef[0];
                        if (variables.ContainsKey(key))
                        {
                            variables[key] = subVarDef[1].Trim();
                        }
                        else
                        {
                            variables.Add(key, subVarDef[1].Trim());
                        }
                    }
                }
            }
            return variables;
        }

        /// <summary>
        /// Processes the conditions in the css code.
        /// </summary>
        /// <param name="css">The CSS.</param>
        /// <param name="variables">The variables.</param>
        /// <returns>the process css file</returns>
        public static string ProcessConditions(string css, Dictionary<string, string> variables)
        {
            foreach (Match match in Regex.Matches(css, "if \\((?<condition>.*?)\\){(?<inner>(?>(?!{|}).|{(?<brack>)|}(?<-brack>))*(?(brack)(?!)))}"))
            {
                string condition = match.Groups["condition"].Value.Trim();
                bool removeCondition = true;

                foreach (string ofConditionPart in condition.Split(new[] { "||" }, StringSplitOptions.RemoveEmptyEntries))
                {
                    bool resultAndCondition = false;
                    foreach (string andConditionPart in ofConditionPart.Split(new[] { "&&" }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        string[] condParts = andConditionPart.Trim().Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
                        if (condParts.Length != 2)
                        {
                            break;
                        }

                        string expectedVar = condParts[0].Trim();
                        if (!variables.ContainsKey(expectedVar))
                        {
                            break;
                        }
                        else
                        {
                            if (variables[expectedVar] == condParts[1].Trim())
                            {
                                resultAndCondition = true;
                            }
                            else
                            {
                                resultAndCondition = false;
                                break;
                            }
                        }
                    }
                    if (resultAndCondition)
                    {
                        removeCondition = false;
                        css = css.Replace(match.Value, match.Groups["inner"].Value);
                        break;
                    }
                }

                // If none of the conditions are true remove all
                if (removeCondition)
                {
                    css = css.Replace(match.Value, string.Empty);
                }
            }

            // Remove tags that are empty, because of inner conditions
            css = Regex.Replace(css, "([^}]*?){}", string.Empty);

            return css;
        }

        /// <summary>
        /// Builds the CSS definition.
        /// </summary>
        /// <param name="css">The CSS.</param>
        /// <returns>A Class containing the defined css</returns>
        public static List<CssDefinition> BuildCssDefinition(string css)
        {
            List<CssDefinition> cssContainer = new List<CssDefinition>();
            foreach (Match match in Regex.Matches(css, "(?<selector>.|[^}]*?)\\{(?<inner>.*?)\\}"))
            {
                CssDefinition cssDefinition = new CssDefinition();
                foreach (string selector in match.Groups["selector"].Value.Split(new[] { ',' }))
                {
                    //if (selector.Contains(":")) // Psuedo
                    //{
                    //    string[] parts = selector.Split(new[] { ':' });
                    //    if (parts.Length != 2) continue;
                    //    cssDefinition.Selectors.Add(new CssSelector { Name = parts[0].Trim(), Pseudo=parts[1].Trim() });
                    //}
                    //else
                    //{
                    //    cssDefinition.Selectors.Add(new CssSelector { Name = selector.Trim() });
                    //}

                    Match matchSelector = Regex.Match(selector, "(?<selector>.[^:|\\(]*)((:(?<pseudo>.*))|(\\((?<params>.*)\\)))?");

                    string sel = (matchSelector.Groups["selector"].Success) ? matchSelector.Groups["selector"].Value : null;
                    string psuedo = (matchSelector.Groups["pseudo"].Success) ? matchSelector.Groups["pseudo"].Value : null;
                    string param = (matchSelector.Groups["params"].Success) ? matchSelector.Groups["params"].Value : null;

                    if (!String.IsNullOrWhiteSpace(sel) && !String.IsNullOrWhiteSpace(psuedo))
                    {
                        cssDefinition.Selectors.Add(new CssSelector { Name = sel.Trim(), Pseudo = psuedo.Trim() });
                    }
                    else if (!String.IsNullOrWhiteSpace(sel) && !String.IsNullOrWhiteSpace(param))
                    {
                        var newsel = new CssSelector { Name = sel.Trim() };
                        foreach (string val in param.Split(new[] { ',' }))
                        {
                            var parts = val.Trim().Split(new[] { ':' });
                            newsel.Parameters.Add(parts[0].Trim(), parts[1].Trim());
                        }
                        cssDefinition.Selectors.Add(newsel);
                    }
                    else if (!String.IsNullOrWhiteSpace(sel))
                    {
                        cssDefinition.Selectors.Add(new CssSelector { Name = sel.Trim() });
                    }


                }
                foreach (string cssdef in match.Groups["inner"].Value.Split(new[] { ';' }))
                {
                    string[] parts = cssdef.Split(new[] { ':' });
                    if (parts.Length != 2) continue;
                    cssDefinition.Properties.Add(new CssProperty { Name = parts[0].Trim(), Value = parts[1].Trim() });
                }
                cssContainer.Add(cssDefinition);
            }
            return cssContainer;
        }

        // TODO: FIX!!!
        public static Dictionary<string, string> CreateUserAgentVariables(string userAgent, Dictionary<string, string> variables)
        {
            Match matchIE = Regex.Match(userAgent, @"^Mozilla[^(]*\([C|c]ompatible;\s*MSIE (?'version'(?'major'\d+)(?'minor'\.\d+)(?'letters'\w*))(?'extra'[^)]*)");
            if (matchIE.Success)
            {
                if (variables.ContainsKey("userAgent.browser"))
                {
                    variables["userAgent.browser"] = "IE";
                }
                else
                {
                    variables.Add("userAgent.browser", "IE");
                }

                if (variables.ContainsKey("userAgent.version"))
                {
                    variables["userAgent.version"] = matchIE.Groups["version"].Value;
                }
                else
                {
                    variables.Add("userAgent.version", matchIE.Groups["version"].Value);
                }

                if (variables.ContainsKey("userAgent.major"))
                {
                    variables["userAgent.major"] = matchIE.Groups["major"].Value;
                }
                else
                {
                    variables.Add("userAgent.major", matchIE.Groups["major"].Value);
                }

                if (variables.ContainsKey("userAgent.minor"))
                {
                    variables["userAgent.minor"] = matchIE.Groups["minor"].Value;
                }
                else
                {
                    variables.Add("userAgent.minor", matchIE.Groups["minor"].Value);
                }

                if (variables.ContainsKey("userAgent.letters"))
                {
                    variables["userAgent.letters"] = matchIE.Groups["letters"].Value;
                }
                else
                {
                    variables.Add("userAgent.letters", matchIE.Groups["letters"].Value);
                }

                if (variables.ContainsKey("userAgent.extra"))
                {
                    variables["userAgent.extra"] = matchIE.Groups["extra"].Value;
                }
                else
                {
                    variables.Add("userAgent.extra", matchIE.Groups["extra"].Value);
                }

                return variables;
            }

            Match matchChrome = Regex.Match(userAgent, "Chrome/(?'version'(?'major'\\d+)(\\.(?'minor'\\d+)?)\\w*)");
            if (matchChrome.Success)
            {
                if (variables.ContainsKey("userAgent.browser"))
                {
                    variables["userAgent.browser"] = "Chrome";
                }
                else
                {
                    variables.Add("userAgent.browser", "Chrome");
                }

                if (variables.ContainsKey("userAgent.version"))
                {
                    variables["userAgent.version"] = matchChrome.Groups["version"].Value;
                }
                else
                {
                    variables.Add("userAgent.version", matchChrome.Groups["version"].Value);
                }

                if (variables.ContainsKey("userAgent.major"))
                {
                    variables["userAgent.major"] = matchChrome.Groups["major"].Value;
                }
                else
                {
                    variables.Add("userAgent.major", matchChrome.Groups["major"].Value);
                }

                if (variables.ContainsKey("userAgent.minor"))
                {
                    variables["userAgent.minor"] = matchChrome.Groups["minor"].Value;
                }
                else
                {
                    variables.Add("userAgent.minor", matchChrome.Groups["minor"].Value);
                }

                return variables;
            }

            return variables;
        }
    }
}
